package org.giovanolli.mumi.controller;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import javax.faces.context.FacesContext;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;

import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.tools.generic.DateTool;
import org.apache.velocity.tools.generic.NumberTool;
import org.giovanolli.mumi.entities.IdentificableEntity;
import org.giovanolli.mumi.exception.NotImplementedMethodException;
import org.giovanolli.mumi.service.Service;
import org.giovanolli.mumi.view.datamodel.ListDataModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.binding.message.MessageBuilder;
import org.springframework.binding.message.MessageContext;
import org.springframework.context.MessageSource;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.velocity.VelocityEngineUtils;
import org.springframework.util.StringUtils;

import com.itextpdf.text.Document;
import com.itextpdf.text.pdf.PdfWriter;
import com.itextpdf.tool.xml.Pipeline;
import com.itextpdf.tool.xml.XMLWorker;
import com.itextpdf.tool.xml.html.Tags;
import com.itextpdf.tool.xml.parser.XMLParser;
import com.itextpdf.tool.xml.pipeline.css.CSSResolver;
import com.itextpdf.tool.xml.pipeline.css.CssResolverPipeline;
import com.itextpdf.tool.xml.pipeline.end.PdfWriterPipeline;
import com.itextpdf.tool.xml.pipeline.html.HtmlPipeline;
import com.itextpdf.tool.xml.pipeline.html.HtmlPipelineContext;

/**
 * The Class AbstractController.
 * 
 * @param <E>
 *            the element type
 */
public abstract class AbstractController<E extends IdentificableEntity> {

	/** The message source. */
	@Autowired
	protected MessageSource messageSource;

	/** The Constant CONFIRMATION_MESSAGE_KEY. */
	protected final static String CONFIRMATION_MESSAGE_KEY = "confirmationMessage";

	/** The velocity engine. */
	@Autowired
	private VelocityEngine velocityEngine;

	/** The date tool. */
	@Autowired
	private DateTool dateTool;

	/** The number tool. */
	@Autowired
	private NumberTool numberTool;

	/** The default css resolver. */
	@Autowired
	private CSSResolver defaultCssResolver;

	/**
	 * Bind entity to model.
	 */
	protected abstract void bindEntityToModel();

	/**
	 * Bind model to entity.
	 */
	protected abstract void bindModelToEntity();

	/**
	 * Confirm save.
	 * 
	 * @param messageContext
	 *            the message context
	 * @param locale
	 *            the locale
	 * @return true, if successful
	 */
	@Transactional(readOnly = true)
	public boolean confirmSave(MessageContext messageContext, Locale locale) {
		bindModelToEntity();
		Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
		Set<ConstraintViolation<E>> constraintViolations = validator.validate(getModel().getEditedEntity());
		if (constraintViolations != null && constraintViolations.size() > 0) {
			for (ConstraintViolation constraintViolation : constraintViolations) {
				messageContext.addMessage(new MessageBuilder().error().source(constraintViolation.getPropertyPath().toString().replace('.', '_'))
				        .defaultText(constraintViolation.getMessage()).build());
			}
			return false;
		}
		save();
		messageContext.addMessage(new MessageBuilder().info().defaultText(messageSource.getMessage(CONFIRMATION_MESSAGE_KEY, null, locale)).build());
		return true;
	}

	/**
	 * Creates the pdf.
	 * 
	 * @throws NotImplementedMethodException
	 *             the not implemented method exception
	 */
	public void createPDF() throws NotImplementedMethodException {
		FacesContext context = FacesContext.getCurrentInstance();
		HttpServletResponse response = (HttpServletResponse) context.getExternalContext().getResponse();
		response.setContentType("application/pdf");
		response.setHeader("Content-disposition", "attachment; filename=" + getExportedFileName() + ".pdf");
		try {

			// step 1
			Document document = new Document();
			// step 2
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			// step 3
			PdfWriter pdfWriter = PdfWriter.getInstance(document, baos);
			document.open();

			Map<String, Object> modelMap = new HashMap<String, Object>();
			modelMap.put("dataModel", getModel().getEditedEntity());
			modelMap.put("dateTool", dateTool);
			modelMap.put("numberTool", numberTool);
			byte[] content = VelocityEngineUtils.mergeTemplateIntoString(velocityEngine, getExportTemplate(), modelMap).getBytes();
			InputStream is = new ByteArrayInputStream(content);

			HtmlPipelineContext htmlContext = new HtmlPipelineContext(null);

			htmlContext.setTagFactory(Tags.getHtmlTagProcessorFactory());

			Pipeline<?> pipeline = new CssResolverPipeline(defaultCssResolver, new HtmlPipeline(htmlContext, new PdfWriterPipeline(document, pdfWriter)));

			XMLWorker worker = new XMLWorker(pipeline, true);
			XMLParser p = new XMLParser(worker);
			p.parse(is);

			document.close();

			// setting some response headers
			response.setHeader("Expires", "0");
			response.setHeader("Cache-Control", "must-revalidate, post-check=0, pre-check=0");
			response.setHeader("Pragma", "public");

			// the contentlength
			response.setContentLength(baos.size());

			// write ByteArrayOutputStream to the ServletOutputStream
			OutputStream os = response.getOutputStream();
			baos.writeTo(os);
			os.flush();
			os.close();

		} catch (IOException e) {
			System.out.println("error: " + e);
		} catch (Exception ex) {
			System.out.println("error: " + ex.getMessage());
		}
		context.responseComplete();
		return;
	}

	/**
	 * Delete.
	 */
	@Transactional(readOnly = true)
	public void delete() {
		performDelete();
	}

	/**
	 * Gets the entity class.
	 * 
	 * @return the entity class
	 */
	protected abstract Class<E> getEntityClass();

	/**
	 * Gets the exported file name.
	 * 
	 * @return the exported file name
	 * @throws NotImplementedMethodException
	 *             the not implemented method exception
	 */
	public String getExportedFileName() throws NotImplementedMethodException {
		throw new NotImplementedMethodException("getExportedFileName");
	}

	/**
	 * Gets the export template.
	 * 
	 * @return the export template
	 * @throws NotImplementedMethodException
	 *             the not implemented method exception
	 */
	public String getExportTemplate() throws NotImplementedMethodException {
		throw new NotImplementedMethodException("getExportTemplate");
	}

	/**
	 * Gets the model.
	 * 
	 * @return the model
	 */
	protected abstract AbstractModel<E> getModel();

	/**
	 * Gets the service.
	 * 
	 * @return the service
	 */
	protected abstract Service<E> getService();

	/**
	 * Inits the.
	 */
	public void init() {
		getModel().setMode(UseCaseMode.SEARCH);
	}

	/**
	 * Perform delete.
	 */
	private void performDelete() {
		getService().deleteEntity(getModel().getEditedEntity());
	}

	/**
	 * Populate non entity properties to model.
	 */
	public void populateNonEntityPropertiesToModel() {

	}

	/**
	 * Prepare entity render.
	 * 
	 * @param uid
	 *            the uid
	 * @param useCaseMode
	 *            the use case mode
	 * @throws InstantiationException
	 *             the instantiation exception
	 * @throws IllegalAccessException
	 *             the illegal access exception
	 */
	@Transactional(readOnly = true)
	public void prepareEntityRender(String uid, UseCaseMode useCaseMode) throws InstantiationException, IllegalAccessException {
		if (((useCaseMode.equals(UseCaseMode.EDIT) || useCaseMode.equals(UseCaseMode.CONSULT)) && !StringUtils.hasText(uid))
		        || (useCaseMode.equals(UseCaseMode.NEW) && StringUtils.hasText(uid)) || useCaseMode.equals(UseCaseMode.SEARCH)) {
			throw new IllegalStateException();
		}
		getModel().setMode(useCaseMode);
		switch (useCaseMode) {
		case NEW:
			getModel().setEditedEntity(getEntityClass().newInstance());
			break;
		case EDIT:
		case CONSULT:
			getModel().setEditedEntity(getService().findByUid(uid));
		case SEARCH:
			break;
		case SELECT:
			break;
		default:
			break;
		}
		bindEntityToModel();
	}

	/**
	 * Save.
	 */
	protected void save() {
		getService().saveEntity(getModel().getEditedEntity());
	}

	/**
	 * Search.
	 */
	@Transactional(readOnly = true)
	public void search() {
		List<E> entityList = getService().getList(getModel().getFilter());
		getModel().setList(new ListDataModel<E>(entityList));
		getModel().setPageNumber(1);
	}
}
